Merged [14230]: Credits updates
[adiumx.git] / Plugins / Contact Status Dock Overlays / AIContactStatusDockOverlaysPlugin.m
blobdfff8e4d6c773d42d75632fe31d773d54fb661f7
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import "AIContactController.h"
18 #import "AIContactStatusDockOverlaysPlugin.h"
19 #import "AIContentController.h"
20 #import "AIDockController.h"
21 #import "AIInterfaceController.h"
22 #import "AIPreferenceController.h"
23 #import "ESContactAlertsController.h"
24 #import <AIUtilities/AIColorAdditions.h>
25 #import <AIUtilities/AIDictionaryAdditions.h>
26 #import <AIUtilities/AIParagraphStyleAdditions.h>
27 #import <AIUtilities/AIArrayAdditions.h>
28 #import <AIUtilities/ESImageAdditions.h>
29 #import <Adium/AIAbstractListController.h>
30 #import <Adium/AIAccount.h>
31 #import <Adium/AIChat.h>
32 #import <Adium/AIIconState.h>
34 #define SMALLESTRADIUS                          15
35 #define RADIUSRANGE                                     36
36 #define SMALLESTFONTSIZE                        14
37 #define FONTSIZERANGE                           30
39 #define DOCK_OVERLAY_ALERT_SHORT        AILocalizedString(@"Display name in the dock icon",nil)
40 #define DOCK_OVERLAY_ALERT_LONG         DOCK_OVERLAY_ALERT_SHORT
42 @interface AIContactStatusDockOverlaysPlugin (PRIVATE)
43 - (void)_setOverlay;
44 - (NSImage *)overlayImageFlash:(BOOL)flash;
45 - (void)preferencesChanged:(NSNotification *)notification;
46 - (void)flushPreferenceColorCache;
47 @end
49 @implementation AIContactStatusDockOverlaysPlugin
51 /*!
52 * @brief Install
53  */
54 - (void)installPlugin
56         overlayObjectsArray = [[NSMutableArray alloc] init];
57     overlayState = nil;
59     //Register as a contact observer (For signed on / signed off)
60     [[adium contactController] registerListObjectObserver:self];
61         
62         //Register as a chat observer (for unviewed content)
63         [[adium contentController] registerChatObserver:self];
64         
65         [[adium notificationCenter] addObserver:self
66                                                                    selector:@selector(chatClosed:)
67                                                                            name:Chat_WillClose
68                                                                          object:nil];
69         
70     //Prefs
71         [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_LIST_THEME];
72         
73     //
74     image1 = [[NSImage alloc] initWithSize:NSMakeSize(128,128)];
75     image2 = [[NSImage alloc] initWithSize:NSMakeSize(128,128)];
76         
77         //Install our contact alert
78         [[adium contactAlertsController] registerActionID:DOCK_OVERLAY_ALERT_IDENTIFIER withHandler:self];
81 - (void)uninstallPlugin
83         [[adium contactController] unregisterListObjectObserver:self];
84         [[adium contentController] unregisterChatObserver:self];
85         [[adium notificationCenter] removeObserver:self];
86         [[adium preferenceController] unregisterPreferenceObserver:self];
89 /*!
90 * @brief Short description
91  * @result A short localized description of the action
92  */
93 - (NSString *)shortDescriptionForActionID:(NSString *)actionID
95         return(DOCK_OVERLAY_ALERT_SHORT);
98 /*!
99 * @brief Long description
100  * @result A longer localized description of the action which should take into account the details dictionary as appropraite.
101  */
102 - (NSString *)longDescriptionForActionID:(NSString *)actionID withDetails:(NSDictionary *)details
104         return(DOCK_OVERLAY_ALERT_LONG);
108 * @brief Image
109  */
110 - (NSImage *)imageForActionID:(NSString *)actionID
112         //XXX
113         return([NSImage imageNamed:@"DockAlert" forClass:[self class]]);
117  * @brief Details pane
118  * @result An <tt>AIModularPane</tt> to use for configuring this action, or nil if no configuration is possible.
119  */
120 - (AIModularPane *)detailsPaneForActionID:(NSString *)actionID
122         return nil;
126 * @brief Perform an action
128  * @param actionID The ID of the action to perform
129  * @param listObject The listObject associated with the event triggering the action. It may be nil
130  * @param details If set by the details pane when the action was created, the details dictionary for this particular action
131  * @param eventID The eventID which triggered this action
132  * @param userInfo Additional information associated with the event; userInfo's type will vary with the actionID.
133  */
134 - (void)performActionID:(NSString *)actionID forListObject:(AIListObject *)listObject withDetails:(NSDictionary *)details triggeringEventID:(NSString *)eventID userInfo:(id)userInfo
136         BOOL isMessageEvent = [[adium contactAlertsController] isMessageEvent:eventID];
137         
138         if(isMessageEvent){
139                 AIChat  *chat;
141                 if((chat = [userInfo objectForKey:@"AIChat"]) &&
142                    (chat != [[adium interfaceController] activeChat]) &&
143                    (![overlayObjectsArray containsObjectIdenticalTo:chat])){
144                         [overlayObjectsArray addObject:chat];
145                         
146                         //Wait until the next run loop so this event is done processing (and our unviewed content count is right)
147                         [self performSelector:@selector(_setOverlay)
148                                            withObject:nil
149                                            afterDelay:0];
151                         /* The chat observer method is responsible for removing this overlay later */
152                 }
154         }else if(listObject){
155                 NSTimer *removeTimer;
156                 
157                 //Clear any current timer for this object o ahve its overlay removed
158                 if(removeTimer = [listObject statusObjectForKey:@"DockOverlayRemoveTimer"]) [removeTimer invalidate];
159                 
160                 //Add a timer to remove this overlay
161                 removeTimer = [NSTimer scheduledTimerWithTimeInterval:5
162                                                                                                            target:self
163                                                                                                          selector:@selector(removeDockOverlay:)
164                                                                                                          userInfo:listObject
165                                                                                                           repeats:NO];
166                 [listObject setStatusObject:removeTimer
167                                                          forKey:@"DockOverlayRemoveTimer"
168                                                          notify:NotifyNever];
170                 if(![overlayObjectsArray containsObject:listObject]){
171                         [overlayObjectsArray addObject:listObject];
172                 }
174                 //Wait until the next run loop so this event is done processing
175                 [self performSelector:@selector(_setOverlay)
176                                    withObject:nil
177                                    afterDelay:0];
178         }
181 - (void)removeDockOverlay:(NSTimer *)removeTimer
183         AIListObject    *inObject = [removeTimer userInfo];
185         [overlayObjectsArray removeObjectIdenticalTo:inObject];
186         
187         [inObject setStatusObject:nil
188                                            forKey:@"DockOverlayRemoveTimer"
189                                            notify:NotifyNever];
190         
191         [self _setOverlay];
194 - (void)chatClosed:(NSNotification *)notification
196         AIChat  *chat = [notification object];
197         
198         [overlayObjectsArray removeObjectIdenticalTo:chat];
199         
200         [self _setOverlay];
204 * @brief Allow multiple actions?
206  * If this method returns YES, every one of this action associated with the triggering event will be executed.
207  * If this method returns NO, only the first will be.
209  * Don't allow multiple dock actions to occur.  While a series of "Bounce every 5 seconds," "Bounce every 10 seconds,"
210  * and so on actions could be combined sanely, a series of "Bounce once" would make the dock go crazy.
211  */
212 - (BOOL)allowMultipleActionsWithID:(NSString *)actionID
214         return(NO);
217 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
218                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
219 {       
220         //Grab colors from status coloring plugin's prefs    
221         [self flushPreferenceColorCache];
222         signedOffColor = [[[prefDict objectForKey:KEY_SIGNED_OFF_COLOR] representedColor] retain];
223         signedOnColor = [[[prefDict objectForKey:KEY_SIGNED_ON_COLOR] representedColor] retain];
224         unviewedContentColor = [[[prefDict objectForKey:KEY_UNVIEWED_COLOR] representedColor] retain];
225         
226         backSignedOffColor = [[[prefDict objectForKey:KEY_LABEL_SIGNED_OFF_COLOR] representedColor] retain];
227         backSignedOnColor = [[[prefDict objectForKey:KEY_LABEL_SIGNED_ON_COLOR] representedColor] retain];
228         backUnviewedContentColor = [[[prefDict objectForKey:KEY_LABEL_UNVIEWED_COLOR] representedColor] retain];
231 - (void)flushPreferenceColorCache
233         [signedOffColor release]; signedOffColor = nil;
234         [signedOnColor release]; signedOnColor = nil;
235         [unviewedContentColor release]; unviewedContentColor = nil;
236         [backSignedOffColor release]; backSignedOffColor = nil;
237         [backSignedOnColor release]; backSignedOnColor = nil;
238         [backUnviewedContentColor release]; backUnviewedContentColor = nil;     
241 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
243         if([inObject isKindOfClass:[AIAccount class]]){
244                 //When an account signs on or off, force an overlay update as it may have silently changed
245                 //contacts' statuses
246                 if([inModifiedKeys containsObject:@"Online"]){
247                         NSEnumerator    *enumerator = [[[overlayObjectsArray copy] autorelease] objectEnumerator];
248                         AIListObject    *listObject;
249                         BOOL                    madeChanges = NO;
250                         
251                         while(listObject = [enumerator nextObject]){
252                                 if(([listObject respondsToSelector:@selector(account)]) &&
253                                    ([(id)listObject account] == inObject) &&
254                                    ([overlayObjectsArray containsObjectIdenticalTo:listObject])){
255                                         [overlayObjectsArray removeObject:listObject];
256                                         madeChanges = YES;
257                                 }
258                         }
259                         
260                         if(madeChanges) [self _setOverlay];
261                 }
262         }
263         
264         return(nil);
268  * @brief When a chat no longer has unviewed content, remove it from display
269  */
270 - (NSSet *)updateChat:(AIChat *)inChat keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
272         if (inModifiedKeys == nil || [inModifiedKeys containsObject:KEY_UNVIEWED_CONTENT]){
273                 
274                 if(![inChat integerStatusObjectForKey:KEY_UNVIEWED_CONTENT]){
275                         if([overlayObjectsArray containsObjectIdenticalTo:inChat]){
276                                 [overlayObjectsArray removeObjectIdenticalTo:inChat];
277                                 [self _setOverlay];
278                         }
279                 }
280         }
281         
282         return nil;
286 - (void)_setOverlay
288     //Remove & release the current overlay state
289     if(overlayState){
290         [[adium dockController] removeIconStateNamed:@"ContactStatusOverlay"];
291         [overlayState release]; overlayState = nil;
292     }
294     //Create & set the new overlay state
295     if([overlayObjectsArray count] != 0){
296         //Set the state
297         overlayState = [[AIIconState alloc] initWithImages:[NSArray arrayWithObjects:[self overlayImageFlash:NO], [self overlayImageFlash:YES], nil]
298                                                                                                          delay:0.5
299                                                                                                    looping:YES 
300                                                                                                    overlay:YES];
301         [[adium dockController] setIconState:overlayState named:@"ContactStatusOverlay"];
302     }   
306 - (NSImage *)overlayImageFlash:(BOOL)flash
308     NSEnumerator                *enumerator;
309     ESObjectWithStatus  *object;
310     NSFont                              *font;
311     NSParagraphStyle    *paragraphStyle;
312     float                               dockIconScale;
313     int                                 iconHeight;
314     float                               top, bottom;
315     NSImage                             *image = (flash ? image1 : image2);
316         
317     //Pre-calc some sizes
318 #warning 1 - [[adium dockController] dockIconScale] is the wrong relationship?
319     dockIconScale = 1- [[adium dockController] dockIconScale];
320     iconHeight = (SMALLESTRADIUS + (RADIUSRANGE * dockIconScale));
322         top = 126;
323         bottom = top - iconHeight;
325     //Set up the string details
326     font = [NSFont boldSystemFontOfSize:(SMALLESTFONTSIZE + (FONTSIZERANGE * dockIconScale))];
327     paragraphStyle = [NSParagraphStyle styleWithAlignment:NSCenterTextAlignment lineBreakMode:NSLineBreakByClipping];
328         
329     //Clear our image
330     [image lockFocus];
331     [[NSColor clearColor] set];
332     NSRectFillUsingOperation(NSMakeRect(0, 0, 128, 128), NSCompositeCopy);
333         
334     //Draw overlays for each contact
335     enumerator = [overlayObjectsArray reverseObjectEnumerator];
336     while((object = [enumerator nextObject]) && !(top < 0) && bottom < 128){
337         float                   left, right, arcRadius, stringInset;
338         NSBezierPath    *path;
339         NSColor                 *backColor = nil, *textColor = nil, *borderColor = nil;
340                 
341         //Create the pill frame
342         arcRadius = (iconHeight / 2.0f);
343         stringInset = (iconHeight / 4.0f);
344         left = 1 + arcRadius;
345         right = 127 - arcRadius;
346                 
347         path = [NSBezierPath bezierPath];
348         [path setLineWidth:((iconHeight/2.0) * 0.13333f)];
349         //Top
350         [path moveToPoint: NSMakePoint(left, top)];
351         [path lineToPoint: NSMakePoint(right, top)];
352                 
353         //Right rounded cap
354         [path appendBezierPathWithArcWithCenter:NSMakePoint(right, top - arcRadius) 
355                                                                                  radius:arcRadius
356                                                                          startAngle:90
357                                                                            endAngle:0
358                                                                           clockwise:YES];
359         [path lineToPoint: NSMakePoint(right + arcRadius, bottom + arcRadius)];
360         [path appendBezierPathWithArcWithCenter:NSMakePoint(right, bottom + arcRadius) 
361                                                                                  radius:arcRadius
362                                                                          startAngle:0
363                                                                            endAngle:270
364                                                                           clockwise:YES];
365                 
366         //Bottom
367         [path moveToPoint: NSMakePoint(right, bottom)];
368         [path lineToPoint: NSMakePoint(left, bottom)];
369                 
370         //Left rounded cap
371         [path appendBezierPathWithArcWithCenter:NSMakePoint(left, bottom + arcRadius)
372                                                                                  radius:arcRadius
373                                                                          startAngle:270
374                                                                            endAngle:180
375                                                                           clockwise:YES];
376         [path lineToPoint: NSMakePoint(left - arcRadius, top - arcRadius)];
377         [path appendBezierPathWithArcWithCenter:NSMakePoint(left, top - arcRadius) radius:arcRadius startAngle:180 endAngle:90 clockwise:YES];
378                 
379                 /*
380                  //Get our colors
381                  if(!([contact integerStatusObjectForKey:KEY_UNVIEWED_CONTENT] && flash)){
382                          backColor = [[contact displayArrayForKey:@"Label Color"] averageColor];
383                          textColor = [[contact displayArrayForKey:@"Text Color"] averageColor];
384                  }
385                  */
386                 
387         if([object integerStatusObjectForKey:KEY_UNVIEWED_CONTENT]){ //Unviewed
388                         if(flash){
389                 backColor = [NSColor whiteColor];
390                 textColor = [NSColor blackColor];
391             }else{
392                 backColor = backUnviewedContentColor;
393                 textColor = unviewedContentColor;
394             }
395         }else if([object integerStatusObjectForKey:@"Signed On"]){ //Signed on
396             backColor = backSignedOnColor;
397             textColor = signedOnColor;
398                         
399         }else if([object integerStatusObjectForKey:@"Signed Off"]){ //Signed off
400             backColor = backSignedOffColor;
401             textColor = signedOffColor;
402                         
403         }
404                 
405                 if(!backColor){
406                         backColor = [NSColor whiteColor];
407                 }
408                 if(!textColor){
409                         textColor = [NSColor blackColor];
410                 }
411                 
412         //Lighten/Darken the back color slightly
413         if([backColor colorIsDark]){
414             backColor = [backColor darkenBy:-0.15];
415             borderColor = [backColor darkenBy:-0.3];
416         }else{
417             backColor = [backColor darkenBy:0.15];
418             borderColor = [backColor darkenBy:0.3];
419         }
420                 
421         //Draw
422         [backColor set];
423         [path fill];
424         [borderColor set];
425         [path stroke];
426                 
427         //Get the object's display name
428         [[object displayName] drawInRect:NSMakeRect(0 + stringInset, bottom + 1, 128 - (stringInset * 2), top - bottom)
429                            withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:font, NSFontAttributeName, paragraphStyle, NSParagraphStyleAttributeName, textColor, NSForegroundColorAttributeName, nil]];
430                 /*        
431                         nameString = [[[NSAttributedString alloc] initWithString:[contact displayName] attributes:[NSDictionary dictionaryWithObjectsAndKeys:font, NSFontAttributeName, paragraphStyle, NSParagraphStyleAttributeName, textColor, NSForegroundColorAttributeName, nil]] autorelease];
432         [nameString drawInRect:NSMakeRect(0 + stringInset, bottom + 1, 128 - (stringInset * 2), top - bottom)];*/
433                 
434         //Move down to the next pill
435                 top -= (iconHeight + 7.0 * dockIconScale);
436                 bottom = top - iconHeight;
437     }
438         
439     [image unlockFocus];
440     
441     return(image);
444 @end